Build Process소스 파일에서 실행 파일로의 빌드 프로세스는 4단계로 구성된다.
- 전처리
- 컴파일
- 어셈블리 및 링크
소스 파일
build_example.cpp
- 전처리 -
전처리한 소스파일
build_example.ii.cpp
- 컴파일 -
build_example.s(assembly code)
- 어셈블리 -
build_example.o(목적 파일)
- 링크 -
build_example(실행파일)
전처리일반적으로 C++은 .cpp, .cxx, .cc, .c++의 확장자 중 하나를 소스파일의 확장자로 사용한다.
buile_example.cpp
#include <iostream>
#include <cmath>
int main(int argc, char* argv[]){
std::cout<<"sqrt(17) is "<<sqrt(17)<<'\n';
}
전처리 과정에서 #include 지시문으로 추가한 모든 파일도 함께 전처리된다.
(g++에서 -E 플래그를 사용하면, 전처리 작업만 처리할 수 있다.)
build_example.ii.cpp
# 1 "build_example.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 450 "<built-in>" 3
# 3 "build_example.cpp" 2
int main(int argc, char* argv[]){
std::cout<<"sqrt(17) is "<<sqrt(17)<<'\n';
}
#include는 재귀적으로 확장하는 재귀적 프로세스이다.(헤더가 포함하는 헤더또한 include 해야함)
단순히 <iostream> 헤더만 포함해도 약 20,000 줄의 프로그램이 된다.
전처리한 프로그램은 일반적으로 .ii 확장자를 추가해서 쓴다.(C는 .i)
전처리 단계에서 #include 뿐만 아니라 매크로(#define)를 확장하고
조건부 코드(#ifdef, #if, #elif, #else, #endif)중에 맞는 조건의 코드를 선택한다.
전처리 단계는 단순히 텍스트를 대체하는 작업으로 대부분 프로그래밍 언어와 독립적임
컴파일(일반적으로 컴파일 한다는 말을 빌드하는 모든 프로세스를 부르는 용어로 사용하지만, 사실은 빌드의 한 단계임)
컴파일은 전처리한 소스 파일을 대상 플랫폼의 어셈블리 코드로 변환한다.
어셈블리 코드는 실제로 수행하는 연산만 포함하기 때문에,
위에서 생성한 전처리한 소스 코드(build_example.ii.cpp)보다 훨씬 짧다.
일반적으로 어셈블리 코드는 .s나 .asm 확장자를 사용한다.
컴파일은 빌드 프로세스 중에서 가장 복잡한 부분이며, C++의 모든 언어 규칙을 적용한다.
내부적으로 프런트 엔드, 미들 엔드, 백 엔드 단계로 나누며, 각 단계는 여러 패스로 구성된다.
이외에도 컴파일 단계에 타입과 네임스페이스 정보로 장식하는 네임 맹글링(Name Mangling)을 포함한다.
(각 코드 파일을 컴파일하면서, 함수 이름과 타입명 등을 변경함-동일한 함수명을 링커로 구분)
g++ -S 옵션을 사용하면, 컴파일까지 진행한 어셈블리 코드를 반환한다.
build_example.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 12, 0 sdk_version 12, 1
.globl _main
.p2align 2
_main:
.cfi_startproc
sub sp, sp, #48
stp x29, x30, [sp, #32]
add x29, sp, #32
어셈블리어셈블리는 컴파일 단계를 통해 생성된 어셈블리 코드를 기계어로 일대일 변환한다.
과정에서 명령은 상대 주소인 16진 코드 및 레이블로 변경된다.
기계어로 변환된 코드를 목적 코드(Object Code)라고 하며, 보통 확장자로 .o를 사용한다.(윈도우는 .obj)
목적 파일은 아카이브(.a, .so, .lib, .dll)로 묶을 수 있다.
해당 프로세스는 C++ 프로그래머에게 완전히 투명한 작업으로 오류가 발생하도록 잘못 처리하는 경우는 없다.
링크어셈블리 과정을 통해 생성된 기계어 코드 오브젝트 파일과 아카이브를 함께 링크(Link)한다.
- 서로 다른 오브젝트 파일의 심볼을 일치시킨다.
- 각 오브젝트 파일의 상대 주소를 애플리케이션 주소 공간에 매핑한다.
링커는 원칙적으로 타입에 대한 개념이 없으며, 심볼을 이름으로만 일치시킨다.
(심볼 이름을 타입 정보로 꾸미기 때문에 링크과정에서 어느정도 타입 안정성을 제공한다.)
네임 맹글링은 함수를 오버로드 했을 때, 함수를 올바른 구현으로 호출할 수 있게끔 링크하는 경우를 감안한다.
링크 라이브러리(아카이브)는 두 가지 방식으로 링크한다.
- 정적(Statically):
아카이브를 실행 파일에 완전히 포함. 정적 링크는 유닉스 시스템의 .a 라이브러리와 윈도우의 .lib 라이브러리 사용
- 동적(Dynamically):
모든 심볼의 존재 여부만 검사하고, 아카이브를 가리키는 일종의 레퍼런스를 유지한다.
동적 링크는 유닉스의 .so 라이브러리와 윈도우의 .dll 라이브러리를 사용한다.
동적 라이브러리와 링크한 실행 파일은 크기가 훨씬 작지만, 바이너리를 실행하는 시스템에 라이브러리가 없으며,
실행할 수가 없다.
(유닉스에서 동적 라이브러리를 찾을 수 없는 경우, 환경변수 LD_LIBRARY_PATH의 검색 경로를 추가할 수 있다.)
LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:경로
export LD_LIBRARY_PATH